﻿using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using System.Text;
using System.Threading;
using Heimdallr.Async;
using Heimdallr.Text;

namespace Heimdallr.Net
{
    /// <summary>
    /// Executes an asynchronized operation of splitting HTTP multipart stream.
    /// </summary>
    /// <remarks>
    /// <para>This class is used by <see cref="HttpMultipartTriggerElement"/>.</para>
    /// <para>For additional information about multipart, see RFC1341 - MIME (Multipurpose Internet Mail Extensions) section 7.2.</para> 
    /// </remarks>
    public class HttpMultipartSplitter : IDisposable
    {
        #region Field

        private const int bufferSize = 65536;

        private readonly BackgroundWorker worker;
        private readonly Encoding encoding;
        private readonly byte[] crlf;
        private readonly int crlfLength;
        private readonly QueueConsumer<MemoryStream, HttpMultipartWorkingItem> queue;

        private HttpMultipartWorkingItem workingItem;
        private bool disposed;

        #endregion

        #region Property

        /// <summary>
        /// Gets a value indicating whether the instance is running an asynchronous operation.
        /// </summary>
        public bool IsBusy
        {
            get { return this.worker.IsBusy; }
        }

        /// <summary>
        /// Gets a value indicating whether the instance has been disposed of.
        /// </summary>
        public bool IsDisposed
        {
            get { return this.disposed; }
        }

        #endregion

        #region Event

        /// <summary>
        /// Occurs when a new part from server has arrived.
        /// </summary>
        public event EventHandler<PartReceivedEventArgs> PartReceived;

        /// <summary>
        /// Occurs when the background operation has completed.
        /// </summary>
        public event EventHandler<CompletedEventArgs> Completed;

        /// <summary>
        /// Occurs when the background operation has raised an exception.
        /// </summary>
        public event EventHandler<ExceptionsEventArgs> Error;

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance of this class.
        /// </summary>
        public HttpMultipartSplitter()
        {
            this.encoding = Encoding.Default;

            this.crlf = this.encoding.GetBytes(Environment.NewLine);
            this.crlfLength = this.crlf.Length;
            this.disposed = false;

            this.worker = new BackgroundWorker();
            this.worker.WorkerSupportsCancellation = true;
            this.worker.DoWork += this.BackgroundWorker_DoWork;
            this.worker.RunWorkerCompleted += this.BackgroundWorker_RunWorkerCompleted;

            this.queue = new QueueConsumer<MemoryStream, HttpMultipartWorkingItem>(this.ProcessDataAction); 
        }

        #endregion

        #region Method

        /// <summary>
        /// Starts an asynchronized operation of splitting HTTP multipart stream, using specified working item.
        /// </summary>
        /// <param name="workingItem">The specified working item.</param> 
        /// <exception cref="ArgumentNullException"><paramref name="workingItem"/> is null.</exception>
        /// <exception cref="InvalidOperationException"><see cref="IsBusy"/> is true.</exception>
        /// <exception cref="ObjectDisposedException">The instance has been disposed.</exception>
        public void Start(HttpMultipartWorkingItem workingItem)
        {
            if (workingItem == null)
                throw new ArgumentNullException("workingItem");

            this.Start(() => workingItem);
        }

        /// <summary>
        /// Starts an asynchronized operation of splitting HTTP multipart stream.
        /// </summary> 
        /// <param name="workingItemFactory"></param>A factory method for creating an instance of <see cref="HttpMultipartWorkingItem"/>.</param> 
        /// <exception cref="ArgumentNullException"><paramref name="workingItemFactory"/> is null.</exception>
        /// <exception cref="InvalidOperationException"><see cref="IsBusy"/> is true.</exception>
        /// <exception cref="ObjectDisposedException">The instance has been disposed.</exception>
        public void Start(Func<HttpMultipartWorkingItem> workingItemFactory)
        {
            if (workingItemFactory == null)
                throw new ArgumentNullException("workingItemFactory");

            if (this.worker.IsBusy)
                throw new InvalidOperationException("Splitter is busy.");

            if (this.disposed)
                throw new ObjectDisposedException(this.GetType().FullName);
             
            this.worker.RunWorkerAsync(workingItemFactory);
        }

        /// <summary>
        /// Stop the asynchronized operation.
        /// </summary>
        public void Stop()
        {
            this.worker.CancelAsync();
        }

        #endregion

        #region BackgroundWorker EventHandler

        private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            Func<HttpMultipartWorkingItem> workingItemFactory = e.Argument as Func<HttpMultipartWorkingItem>;

            try
            {
                Interlocked.Exchange(ref this.workingItem, workingItemFactory());
            }
            catch (Exception error)
            {
                throw new InvalidOperationException("Can not create HttpMultipartWorkingItem instance.", error);
            }

            if (this.workingItem == null)
            {
                e.Cancel = true;
                return;
            }

            Lazy<MemoryStream> part = new Lazy<MemoryStream>();

            byte[] boundary = this.encoding.GetBytes(workingItem.Boundary);
            int maxPartLength = this.workingItem.MaxPartLength;
            int boundaryLength = boundary.Length;

            byte[] buffer = new byte[bufferSize];
            int bufferOffset = 0, bufferLength = 0, boundaryIndex = 0;
            int partStartIndex = -1;
            int dataLength = 0;

            using (Stream source = this.workingItem.Source)
            {
                // Keep reading data from source.
                while (!this.worker.CancellationPending)
                {
                    // If buffer has no room for incoming data.
                    if (bufferOffset == bufferSize)
                    {
                        part.Value.Write(buffer, 0, bufferOffset);
                        bufferOffset = 0;
                        partStartIndex = -1;
                    }

                    try
                    {
                        bufferLength = source.Read(buffer, bufferOffset, bufferSize - bufferOffset);
                    }
                    catch (Exception error)
                    {
                        if (part.IsValueCreated)
                            part.Value.Dispose();

                        throw new Exception("An error has occurred from current connection.", error);
                    }

                    // If there's no more data from source.
                    if (bufferLength == 0)
                    {
                        // If there's still remaining data in buffer.
                        if (bufferOffset > 0)
                        {
                            part.Value.Write(buffer, 0, bufferOffset);

                            // Enqueue data to process.
                            if (part.Value.Length < maxPartLength)
                                this.queue.EnqueueAndStart(part.Value, this.workingItem);
                            else
                                part.Value.Dispose();
                        }
                    }

                    bufferLength += bufferOffset;

                    // The length of data just read must greater than boundary length.
                    if (bufferLength < boundaryLength)
                    {
                        bufferOffset = bufferLength;
                        continue;
                    }

                    // Keep finding boundary out.
                    while (!this.worker.CancellationPending)
                    {
                        // Find the next boundary out with Morris-Pratt algorithm in buffer.
                        if (partStartIndex >= 0)
                            boundaryIndex = buffer.MorrisPrattSearchFirst(boundary, partStartIndex, bufferLength - partStartIndex);
                        else
                            boundaryIndex = buffer.MorrisPrattSearchFirst(boundary, 0, bufferLength);

                        // If boundary exists in buffer.
                        if (boundaryIndex >= 0)
                        {
                            if (partStartIndex > 0)
                                dataLength = boundaryIndex - partStartIndex - this.crlfLength;
                            else
                                dataLength = boundaryIndex - this.crlfLength;

                            if (dataLength > 0)
                            {
                                if (partStartIndex > 0)
                                    part.Value.Write(buffer, partStartIndex, dataLength);
                                else
                                    part.Value.Write(buffer, 0, dataLength);

                                if (part.Value.Length < maxPartLength)
                                    this.queue.EnqueueAndStart(part.Value, this.workingItem);
                                else
                                    part.Value.Dispose();

                                part = new Lazy<MemoryStream>();
                            }

                            // If boundary exists, indicate the start index of next part.
                            partStartIndex = boundaryIndex + boundaryLength + this.crlfLength;

                            // If the start index of next part is not in current buffer.
                            if (partStartIndex >= bufferLength)
                            {
                                // Continue reading data from source.
                                bufferOffset = partStartIndex;
                                break;
                            }
                        }
                        else
                        {
                            // If the start index of part exists in current buffer.
                            if (partStartIndex >= 0)
                            {
                                // Move all part data to the front of buffer.
                                bufferOffset = bufferLength - partStartIndex;
                                Array.Copy(buffer, partStartIndex, buffer, 0, bufferOffset);

                                // Reset the start index of part in buffer to zero.
                                partStartIndex = 0;
                            }
                            else
                            { 
                                // Extract some data to write to memory stream, and let the remaining data length equal to boundary length.
                                // This is because of the position of boundary may "overstride" two parts..
                                bufferOffset = boundaryLength;
                                part.Value.Write(buffer, 0, bufferLength - boundaryLength);

                                // Move all remaining part data to the front of buffer.
                                Array.Copy(buffer, bufferLength - boundaryLength, buffer, 0, boundaryLength);
                            }

                            // Continue reading data from source.
                            break;
                        }
                    }
                }
            }
        }

        private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            object userState = this.workingItem != null ? this.workingItem.UserState : null;

            if (e.Error != null)
                this.OnError(new ExceptionsEventArgs(e.Error, userState));
            else
                this.OnCompleted(new CompletedEventArgs(userState));

            Interlocked.Exchange(ref this.workingItem, null);
        }

        #endregion
         
        #region Others

        private void ProcessDataAction(MemoryStream ms, HttpMultipartWorkingItem item)
        {
            using (ms)
            {
                PartReceivedEventArgs e = this.GenerateEventArgs(ms.ToArray(), item.UserState);

                if (e != null)
                    this.OnPartReceived(e);
            }
        }

        private PartReceivedEventArgs GenerateEventArgs(byte[] buffer, object userState)
        {
            NameValueCollection headers = new NameValueCollection();
              
            int bufferLength = buffer.Length;
            int crlfIndex = 0;
            int headerStartIndex = 0;
            int headerLength = 0;
            bool endOfHeaders = false;
             
            while (!this.worker.CancellationPending)
            {
                crlfIndex = this.IndexOfCrlf(buffer, headerStartIndex, bufferLength);
                headerLength = crlfIndex - headerStartIndex;

                if (headerLength > 0)
                {
                    string[] header = this.encoding.GetString(buffer, headerStartIndex, headerLength).Split(':');

                    if (header.Length != 2) 
                        break;

                    headers.Add(header[0], header[1].Trim());
                    headerStartIndex = crlfIndex + this.crlfLength;
                }
                else
                {
                    if (endOfHeaders)
                    {
                        int dataIndex = crlfIndex + this.crlfLength;
                        int dataLength = bufferLength - dataIndex;
                        byte[] data = new byte[dataLength];

                        Array.Copy(buffer, dataIndex, data, 0, dataLength);

                        return new PartReceivedEventArgs(headers, data, userState);
                    }
                    endOfHeaders = true;
                }
            }

            return null;
        }

        private int IndexOfCrlf(byte[] buffer, int startIndex, int bufferLength)
        {
            for (int i = startIndex; i < bufferLength; i++)
                for (int j = 0; j < this.crlfLength; j++)
                    if (buffer[i] == this.crlf[j] && j + 1 == this.crlfLength)
                        return i - this.crlfLength + 1;

            return -1;
        }

        #endregion

        #region Raise Event Method

        /// <summary>
        /// Raise the <see cref="PartReceived"/> event. This method is invoked whenever a new part from server has arrived.
        /// </summary>
        /// <param name="e">The event data that contains headers and body.</param>
        /// <exception cref="ArgumentNullException">Parameter <paramref name="e"/> is null.</exception>
        protected virtual void OnPartReceived(PartReceivedEventArgs e)
        {
            if (e == null)
                throw new ArgumentNullException("e");

            if (this.PartReceived != null)
                this.PartReceived(this, e);
        }

        /// <summary>
        /// Raise the <see cref="Completed"/> event. This method is invoked whenever the asynchronized operation is completed.
        /// </summary>
        /// <param name="e">Event data for the event..</param>
        /// <exception cref="ArgumentNullException">Parameter <paramref name="e"/> is null.</exception>
        protected virtual void OnCompleted(CompletedEventArgs e)
        {
            if (e == null)
                throw new ArgumentNullException("e");

            if (this.Completed != null)
                this.Completed(this, e);
        }

        /// <summary>
        /// Raises the <see cref="Error"/> event.
        /// </summary>
        /// <param name="e">Event data for the event.</param>
        /// <exception cref="ArgumentNullException">Parameter <paramref name="e"/> is null.</exception>
        protected virtual void OnError(ExceptionsEventArgs e)
        {
            if (e == null)
                throw new ArgumentNullException("e");

            if (this.Error != null)
                this.Error(this, e);
        }

        #endregion

        #region IDisposable Member

        /// <summary>
        /// Disposes this instance.
        /// </summary>
        public void Dispose()
        {
            this.disposed = true;
            this.worker.Dispose();
        }

        #endregion
    }
}